package com.lijiajia.cloud.authserver.security.oauth;

import cn.hutool.core.util.IdUtil;
import com.lijiajia.cloud.authserver.pojo.mongodb.User;
import com.lijiajia.cloud.authserver.pojo.mongodb.UserConnection;
import com.lijiajia.cloud.authserver.security.BaseUser;
import com.lijiajia.cloud.authserver.security.BaseUserService;
import com.lijiajia.cloud.authserver.security.OAuthUser;
import com.lijiajia.cloud.authserver.service.MenuService;
import com.lijiajia.cloud.authserver.service.RoleService;
import com.lijiajia.cloud.authserver.service.UserConnectionService;
import com.lijiajia.cloud.authserver.service.UserService;
import com.lijiajia.cloud.authserver.security.oauth.user.UserInfoConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;
import java.util.Map;

/**
 * 自定义 oauth2 用户服务
 *
 * @author lijiajia
 * @since 2019-05-21
 */
public class CustomOAuth2UserService extends BaseUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";

    private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";

    private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";

    private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
            new ParameterizedTypeReference<>() {
            };

    private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();

    private RestOperations restOperations;

    private Map<String, UserInfoConverter> userInfoConverters = Collections.emptyMap();

    private final UserService userService;
    private final UserConnectionService userConnectionService;

    public CustomOAuth2UserService(UserService userService,
                                   RoleService roleService,
                                   MenuService menuService,
                                   UserConnectionService userConnectionService) {
        this(userService, roleService, menuService, userConnectionService, Collections.emptyMap());
    }

    public CustomOAuth2UserService(UserService userService,
                                   RoleService roleService,
                                   MenuService menuService,
                                   UserConnectionService userConnectionService,
                                   Map<String, UserInfoConverter> userInfoConverters) {
        super(roleService, menuService);
        this.userService = userService;
        this.userConnectionService = userConnectionService;
        this.userInfoConverters = userInfoConverters;
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        this.restOperations = restTemplate;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        Assert.notNull(userRequest, "userRequest不能为null");
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        if (!StringUtils.hasText(userRequest.getClientRegistration().getClientId())) {
            OAuth2Error oauth2Error = new OAuth2Error(
                    MISSING_USER_INFO_URI_ERROR_CODE,
                    "在UserInfoEndpoint中缺少客户端注册所需的UserInfoUri: " +
                            userRequest.getClientRegistration().getRegistrationId(),
                    null
            );
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
            OAuth2Error oauth2Error = new OAuth2Error(
                    MISSING_USER_INFO_URI_ERROR_CODE,
                    "在UserInfoEndpoint中缺少客户端注册所需的UserInfoUri: " +
                            userRequest.getClientRegistration().getRegistrationId(),
                    null
            );
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        String nameAttributeKey = userRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();
        if (!StringUtils.hasText(nameAttributeKey)) {
            OAuth2Error oauth2Error = new OAuth2Error(
                    MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
                    "在UserInfoEndpoint中缺少客户端注册所需的 \"用户名\" 属性名称: " +
                            userRequest.getClientRegistration().getRegistrationId(),
                    null
            );
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }

        RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);

        ResponseEntity<Map<String, Object>> response;
        try {
            assert request != null;
            response = this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
        } catch (OAuth2AuthorizationException ex) {
            OAuth2Error oauth2Error = ex.getError();
            String errorDetails = String.format("Error details:[UserInfo Uri: %s, Error Code: %s, Error Description: %s]",
                    userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri(),
                    oauth2Error.getErrorCode(),
                    oauth2Error.getDescription());
            oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails, null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
        } catch (RestClientException ex) {
            OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
        }
        Map<String, Object> userAttributes = response.getBody();

        assert userAttributes != null;
        if (!userAttributes.containsKey(nameAttributeKey)) {
            throw new IllegalArgumentException("Missing attribute '" + nameAttributeKey + "' in attributes");
        }
        String openId = userAttributes.get(nameAttributeKey).toString();

        OidcUserInfo userInfo;
        if (userInfoConverters.containsKey(registrationId)) {
            UserInfoConverter userInfoConverter = userInfoConverters.get(registrationId);
            userInfo = userInfoConverter.convert(userAttributes);
        } else {
            userInfo = new OidcUserInfo(userAttributes);
        }

        User user = userService.findByOpenId(registrationId, openId)
                .orElseGet(() -> createUser(userInfo, registrationId, openId));
        return buildUser(user, userAttributes, userInfo);
    }

    /**
     * 构建 user 对象
     *
     * @param user           数据库用户
     * @param userAttributes oauth用户信息
     * @return OAuth User
     */
    protected OAuthUser buildUser(User user, Map<String, Object> userAttributes, OidcUserInfo userInfo) {
        BaseUser baseUser = super.buildUser(user);
        return new OAuthUser(baseUser, userAttributes, userInfo);
    }

    /**
     * 第三方登陆
     * 创建新用户 by userInfo
     *
     * @param userInfo       oauth用户信息
     * @param registrationId oauth提供商id
     * @param openId         openId
     * @return 创建的新用户
     */
    @Transactional
    protected User createUser(OidcUserInfo userInfo, String registrationId, String openId) {
        User user = new User()
                .setId(IdUtil.simpleUUID())
                .setNickname(userInfo.getNickName())
                .setTelephone(userInfo.getPhoneNumberVerified() ? userInfo.getPhoneNumber() : null)
                .setEmail(userInfo.getEmailVerified() ? userInfo.getEmail() : null)
                .setAvatar(userInfo.getPicture())
                .setEnabled(true)
                .setDeleted(false);
        userService.insert(user);

        Long rank = userConnectionService.findMaxRank(user.getId(), registrationId).orElse(1L) + 1L;
        UserConnection connection = new UserConnection();
        connection.setUserId(user.getId())
                .setProviderId(registrationId)
                .setOpenId(openId)
                .setRank(rank)
                .setNickname(userInfo.getNickName())
                .setPicture(userInfo.getPicture())
                .setProfile(userInfo.getProfile());
        userConnectionService.insert(connection);
        return user;
    }

    /**
     * Sets the {@link Converter} used for converting the {@link OAuth2UserRequest}
     * to a {@link RequestEntity} representation of the UserInfo Request.
     *
     * @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the UserInfo Request
     * @since 5.1
     */
    public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
        Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
        this.requestEntityConverter = requestEntityConverter;
    }

    /**
     * Sets the {@link RestOperations} used when requesting the UserInfo resource.
     *
     * <p>
     * <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
     * <ol>
     * <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
     * </ol>
     *
     * @param restOperations the {@link RestOperations} used when requesting the UserInfo resource
     * @since 5.1
     */
    public final void setRestOperations(RestOperations restOperations) {
        Assert.notNull(restOperations, "restOperations cannot be null");
        this.restOperations = restOperations;
    }
}
